LINE ミニアプリから Web API をコールする時にハマった話

LINE ミニアプリから Web API をコールする時にハマった話

LINE ミニアプリから外部サイトの API コールを行ってハマった話を記載します。ご存じの方でしたら当たり前のことかもしれませんが…
Clock Icon2024.10.10

こんにちは、高崎@アノテーション です。

はじめに

前回の記事(LINE の友だち登録経路を拾える仕組みを考えてみた)において作成した環境ですが、動作確認をするに辺りとあるエラーに苛まれたため、その内容と対応策を記事にしました。

なお、断りの無い限りベースソースはこの記事のソースを使用しています。

発生したエラー

用意したボタンをタップしても反応せず、VConsole が動作していたので中身を確認し、エラー内容をコピペすると下記が発生しました。

Access to XMLHttpRequest at 'https://API Gateway のアドレス/register' from origin 'https://LINE ミニアプリを呼び出す CloudFront で割り振られたアドレス' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

はい、LINE ミニアプリから API Gateway へのアクセスが CORS ポリシーに抵触したためハネられたんですね。

CORS とは

CORS は Cross-Over-Resource-Sharing の略で、異なるオリジンからの Web アクセスを保護する仕組みで、下記のサイトに詳しく内容が載っています。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

要求元のオリジンと要求先のオリジンが違う場合、要求先にて Access-Control-Allow-Origin ヘッダ要素をレスポンスヘッダに付加して返却するのですが、ヘッダが無い場合や Allow されているオリジンと違っている場合はアクセスが拒否される、という仕組みです。

今回のケースは、そもそもレスポンスヘッダに記載が無かったためエラーとして返却されました。

ヘッダを付けて返却するには

対象のオリジンは API Gateway 経由で処理されます Lambda の処理にてその仕組みを実装することになるのですが、

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html

この資料を元に、今回構築しているプログラムはプロキシ統合しているので、

資料に書いてあったサンプルプログラム
export const handler = async (event) => {
    const response = {
        statusCode: 200,
        headers: {
            "Access-Control-Allow-Headers" : "Content-Type",
            "Access-Control-Allow-Origin": "https://www.example.com",
            "Access-Control-Allow-Methods": "OPTIONS,POST,GET"
        },
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

と、レスポンスに付加するだけで対処できます。

今回の環境に適用する場合はこんな感じです。

LineBotTest_backup/backend/src/handler/liff-app/handler-register.ts
import { ID_REGISTER_API_USE_CASE, initContainer } from "@/di-container/register-container";
import { RegisterApiUseCase, UnexpectedError } from "@/use-case/liff-app-use-case/use-case";
import { APIGatewayProxyResult, APIGatewayProxyEvent, Context } from "aws-lambda";

const resultOK: APIGatewayProxyResult = {
  statusCode: 200,
  headers: {
+   "Access-Control-Allow-Headers" : "Content-Type",
+   "Access-Control-Allow-Origin": `${process.env.DEF_CLOUD_FRONT_ORIGIN}`,
+   "Access-Control-Allow-Method": "OPTIONS,POST,GET",
  },
  body: JSON.stringify({}),
}

const resultError: APIGatewayProxyResult = {
  statusCode: 500,
  headers: {
+   "Access-Control-Allow-Headers" : "Content-Type",
+   "Access-Control-Allow-Origin": `${process.env.DEF_CLOUD_FRONT_ORIGIN}`,
+   "Access-Control-Allow-Method": "OPTIONS,POST,GET",
  },
  body: JSON.stringify({}),
}
 

オリジンのアドレスは CloudFront で示したアドレスになるのですが、Lambda の環境変数に設定する実装にしています。[1]

追加するのはこれだけではない

CORS のアクセスには 単純リクエスト単純ではないリクエスト があり、クライアント側で設定します。

単純リクエストで行う場合

axiosで送るときのサンプル
        const responsePreRegister = await axios.post(`${import.meta.env.VITE_PREREGISTER_API_URL}`, {
          lineAccessToken, registeredSource: paramRegisteredSource
+       }, {
+         headers: {
+           "Content-Type": "application/x-www-form-urlencoded",
+         },
        });

サンプルはコンテントタイプを指定して単純リクエストにする形ですが、今回のケースではバックエンド側には、

パラメータサンプル
  body: 'lineAccessToken=アクセストークンの文字&registeredSource=登録経路パラメータ',

…と、パラメータが JSON ではなくクエリパラメータで渡されるため、改めてバックエンド側でパラメータを分解する実装が必要ですが、今後パラメータが増減することを鑑みて JSON で進めたいと考えていたため、今回は 単純ではないリクエスト で実装します。

単純ではないリクエストで実現する

こちらのサイトでの説明 にもあります通り、単純ではないリクエストは プリフライトリクエスト が要求されます。

サイトに記載されていたシーケンスを記載しますと下記になります。

OPTIONS を受け付ける必要があります。

方法は以下が考えられます。

  1. API に OPTIONS のメソッドを追加してハンドラに同じように CORS を許可しているオリジンや Method を定義したレスポンスを返すよう実装する。
  2. API Gateway の設定に CORS 許可を追加する。

コスト的には 2. の方が良いかと思いますので今回はこの方法を使用します。

実装

今回の環境の IaC ソースに追加する実装を下記を追加します。

先ほど記載した Lambda に環境変数を追加する 旨の実装も記載しています。

LineBotTest_backup/iac/lib/line_bot_test-stack.ts
 import * as logs from "aws-cdk-lib/aws-logs";
+import dotenv from "dotenv";
 export class LineBotTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

+   dotenv.config();
    // example resource
     const lambdaLiffApp = new lambdaNodejs.NodejsFunction(this, "LiffTest", {
      runtime: lambda.Runtime.NODEJS_20_X,
      entry: "../backend/src/handler/liff-app/handler-register.ts",
      role: roleLiffApp,
      timeout: cdk.Duration.seconds(120),
+     environment: {
+       "DEF_CLOUD_FRONT_ORIGIN": `${process.env.DEF_ORIGIN_LIFF_URL}`,
+     }
    });
     // API Gateway の POST イベントと Lambda との紐付け
    lineWebhook.addMethod("POST", lambdaIntegMemoBot);
    // Liff App 用
    const register = api.root.addResource("register", {
+     // CORS のプレフライトを自動受信する設定
+     defaultCorsPreflightOptions: {
+       allowOrigins: [`${process.env.DEF_ORIGIN_LIFF_URL}`],
+       allowMethods: apigateway.Cors.ALL_METHODS,
+       allowHeaders: apigateway.Cors.DEFAULT_HEADERS,
+       statusCode: 200,
+     },
    });
    register.addMethod("POST", lambdaIntegLiffApp);
 

動作確認

Chrome の検証メニューでネットワークのやり取りを確認できますので見てみます。

プレフライト(OPTIONS)のやり取り
preflight-header

POST のやり取り
POST-header

ヘッダに設定され、無事オリジンの異なる間柄でのやり取りが出来ました。

おわりに

今回は CORS ポリシーに抵触して発生したエラーに関しての対処を記事にいたしました。

CORS はサーバー側の設定の問題かもしれませんが、クライアント側で CORS を回避できないか?を試しに弊社 Slack に導入され生成 AI bot に確認してみたところ…。

AIChat

はい、やんわり怒られてしまいました。
そりゃそうですね…。

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。

サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。

当社は様々な職種でメンバーを募集しています。

「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイト をぜひご覧ください。

脚注
  1. Allow を * に設定することも可能ですが、セキュリティ上微妙だと判断し今回は要求元の CloudFront のアドレスのみ指定しています。 ↩︎

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.